Redis 持久化
AOF
AOF 日志
- AOF(Append Only File):Redis 每执行一条命令,就把该命令以追加的方式写入一个文件中,但是指挥记录写操作命令,不会记录读操作命令
- Redis 先执行写操作,然后再将命令记录到 AOF 日志,这样做有个好处
- 避免额外的检查开销:先记录再执行的话,需要对命令进行语法检查,如果不进行语法检查,可能导致恢复时出错;先执行再记录,只有命令成功执行时才会记录,可以保证 AOF 日志中的命令都是正确可执行的
- 不会阻塞当前写操作命令的执行:写操作执行成功后才会将命令记录到 AOF 日志中,因此不会阻塞当前写操作
- 存在风险
- 执行写操作和记录日志是两个过程,如果 Redis 宕机时还没来得及将操作写入 AOF 日志,就有数据丢失的可能
- 写操作执行成功后才记录日志,不会阻塞当前写操作,但是有可能阻塞后续的操作,因为命令写入日志的操作也是在主进程执行的,如果磁盘 I/O 压力大,导致写入速度慢,进而阻塞后续操作
- AOF 写入过程
写回策略
三种写回策略
- Always:每次执行完写操作,同步将 AOF 日志数据写回硬盘
- Everysec:每次执行完写操作,先将命令写入 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区中的内容写回硬盘
- No:不由 Redis 控制写回硬盘的时机,交给操作系统控制,即每次执行完写操作,将命令写入 AOF 文件的内核缓冲区,然后由操作系统决定何时将缓冲区内容写回硬盘
三种写回策略无法完美解决 主进程阻塞 和 减少数据丢失 的问题:
- Always 策略可以最大程度保证数据不丢失,但是会影响主进程性能(高可靠)
- No 策略性能较好,但是写回硬盘时间不确定,因此宕机时可能丢失的数据量无法确定(高性能)
- Everysec 策略较为折中,避免了较大的性能开销,也比较能避免数据丢失(折中)
策略实现机制
三种策略的本质就是在控制 fsync() 函数的调用时机,当应用程序向文件写入数据时,内核通常先将数据复制到内核缓冲区,然后排入队列,最后由内核决定何时写入硬盘
重写机制
AOF 文件随着写操作命令增多,文件会越来越大,过大可能带来性能问题,比如 Redis 重启恢复数据时恢复速度慢,因此 Redis 提供了 AOF 重写机制,AOF 文件超过阈值时会对 AOF 文件重写进行压缩
重写机制就是在重写是,读取当前数据库内所有的键值对,然后将每一个键值对用一条命令记录到新的 AOF 文件
,等到全部记录完后,使用新的 AOF 文件替换现有的 AOF 文件
不复用现有 AOF 文件的原因:防止 AOF 文件重写失败,造成对现有 AOF 文件的污染AOF 后台重写
AOF 文件重写的过程是十分耗时的,因此不能放在主进程中进行,而是由后台子进程bgrewriteaof
完成,这样做的好处在于:- 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,避免阻塞主进程
- 子进程带有主进程的数据副本
使用子进程而非线程的的原因:多线程之间会共享内存,修改共享内存的数据时需要通过加锁来保证数据的安全,因此会降低性能;而使用子进程的话,在创建子进程时,父子进程是共享内存数据的,但是贡献的内存只能以 只读 的方式,当父子进程任意一方修改共享内存,就会发生写时复制,父子进程各自用用独立的数据副本,不用加锁保证数据安全
子进程如何拥有父进程的数据副本
主进程通过 fork 系统调用生成bgrewriteaof
子进程时,操作系统就会把主进程的 页表 复制一份给子进程,页表记录着虚拟地址和物理地址的映射关系,而不会复制物理内存,即两者的虚拟空间不同,但对应的物理空间是同一个
子进程共享父进程的物理内存数据,能够 节省物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读
当父进程或者子进程对这个内存发起写操作时,CPU 会触发 写保护中断(由于违反只读权限导致),然后操作系统会在 写保护中断处理函数 里进行 物理内存复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为 可读写,最后才会对内存进行写操作,这个过程称为 写时复制(Copy On Write)
写时复制:发生写操作时,操作系统才会复制物理内存,可以防止 fork 子进程时,由于物理内存数据的复制时间过长导致父进程长时间阻塞
操作系统复制父进程页表的时候父进程也是阻塞的,但是页表的大小相比于实际物理内存小很多,因此过程比较快,但是如果父进程的内存数据非常大,页表也会很大,此时父进程通过 fork 创建子进程阻塞的时间也越久
导致阻塞父进程的两个阶段:- 创建子进程时复制父进程的页表等数据结构,阻塞时间跟页表大小相关,页表越大,阻塞时间越长
- 创建完子进程后,如果父进程或子进程修改了共享数据,就会发生 写时复制,期间会拷贝物理内存,内存越大,阻塞时间越长
子进程重写过程中父进程可以正常处理命令,如果此时主进程修改了已经存在的 key-value,就会发生写时复制,但是 只会复制主进程修改的物理内存,没有修改的物理内存仍然和子进程共享,如果这个阶段修改的是一个 bigkey,这时复制的物理内存数据就会比较耗时,有阻塞主进程的风险
父子进程数据不一致
主进程修改了已经存在的 key-value,此时这个数据在子进程的内存数据和父进程不一致
Redis 设置了一个 AOF 重写缓冲区来解决这个问题,这个缓冲区在创建 bgrewriteaof 子进程后开始使用,在重写期间,当 Redis 执行完一个写命令后,会同时将这个写命令写入AOF 缓冲区
和AOF 重写缓冲区
,当子进程完成 AOF 重写工作后,会向主进程发送一条信号,主进程收到信号后会调用一个信号处理函数,完成以下工作- 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 文件中,使新旧两个 AOF 文件保存的数据库状态一致
- 将新的 AOF 文件改名,覆盖现有的 AOF 文件
而在 AOF 重写期间主进程需要执行以下工作:
- 执行客户端发来的命令
- 将执行后的写命令追加到 AOF 缓冲区
- 将执行后的写命令追加到 AOF 重写缓冲区
在整个 AOF 重写过程中,除了发生写时复制会对主线程造成阻塞,信号处理函数执行时也会对主线程造成阻塞,其他时候,AOF 重写都不会阻塞主线程
RDB
RDB 快照
AOF 文件的内容是 操作命令,而 RDB 快照的内容是 二进制数据
RDB 快照记录的是 某一个瞬间的内存数据(实际数据),而 AOF 文件记录的是命令操作的日志(非实际数据),因此在数据恢复时,RDB 的效率高于 AOFRedis 提供了两个命令来生成 RDB 文件:
save
和bgsave
,他们的区别在于是否在主线程中执行- save:在主线程生成 RDB 文件,如果写入时间太长,会阻塞主线程
- bgsave:创建子进程用于生成 RDB 文件,可以避免主线程阻塞
RDB 文件的加载工作在服务器启动时自动执行,Redis 没有提供专门用于加载 RDB 文件的命令
RDB 的快照是全量快照,每次执行都是把内存中所有的数据记录到磁盘中,是一个比较重的操作,频率太高可能影响 Redis 性能,频率太低,可能在宕机时丢失更多数据,这也是 RDB 快照的缺点
执行快照时的数据修改
- Redis 执行 bgsave 时继续处理操作命令的关键在于 写时复制技术(Copy-On-Write,COW),在执行 bgsave 命令时会通过 fork() 创建子进程,此时父子进程共享同一片内存数据,同 AOF
- 父子进程数据不一致
发生了写时复制后,RDB 快照文件保存的是原本的内存数据,而主线程刚修改的数据只能由下一次快照保存
因此如果在快照创建完毕后 Redis 崩溃,将会丢失这个期间主线程修改的数据 - 极端情况
在执行 RDB 快照持久化期间,所有的共享内存均被修改,此时占用的内存会是原先的两倍,因此在写操作多的场景下需要留意快照过程内存变化,防止内存溢出
AOF + RDB
- Redis 4.0 提出混合持久化,将 RDB 和 AOF 缓和使用
aof-use-rdb-preamble yes
- 混合持久化工作在 AOF 日志重写期间
在 AOF 重写日志时,fork 出来的子进程会先将共享的内存数据以 RDB 方式写入 AOF 文件,而主线程处理的操作命令会被记录在 AOF 重写缓冲区,重写缓冲区中的增量命令会以 AOF 方式写入 AOF 文件,写入完成后通知主线程用新的 混合持久化文件 替换旧的 AOF 文件
使用了混合持久化之后,AOF 文件前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据 - 优点
重启时加载速度快,宕机时丢失数据少
bigkey 对持久化的影响
- bigkey 占用的内存较大,会导致持久化时对页表和内存数据的复制比较耗时,可能导致主线程阻塞
- 如何避免 bigkey
在设计阶段将 bigkey 拆分,或者定时检查是否存在 bigkey,对于可删除的 bigkey 使用 unlink 命令进行异步删除,避免阻塞主线程